package main

import (
	"bytes"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"gopkg.in/yaml.v2"
)

// Config 配置结构体
type Config struct {
	Camera    CameraConfig    `yaml:"camera"`
	Server    ServerConfig    `yaml:"server"`
	Ffmpeg    FfmpegConfig    `yaml:"ffmpeg"`
	Logging   LoggingConfig   `yaml:"logging"`
	Detection DetectionConfig `yaml:"detection"`
}

type CameraConfig struct {
	RtspURL string `yaml:"rtsp_url"`
}

type ServerConfig struct {
	RtspURL string `yaml:"rtsp_url"`
}

type FfmpegConfig struct {
	Path string `yaml:"path"`
}

type LoggingConfig struct {
	Dir string `yaml:"dir"`
}

type DetectionConfig struct {
	Method string `yaml:"method"` // 检测方式，可选值为 "ffmpeg" 或 "ping"
}

// 加载 YAML 配置文件
func loadConfig(filePath string) (*Config, error) {
	data, err := os.ReadFile(filePath)
	if err != nil {
		return nil, fmt.Errorf("读取文件配置失败: %v", err)
	}

	var config Config
	err = yaml.Unmarshal(data, &config)
	if err != nil {
		return nil, fmt.Errorf("解析配置文件失败: %v", err)
	}

	return &config, nil
}

// 检测摄像头是否在线
func checkOnlineByFFMPEG(ffmpegPath, rtspURL string) bool {
	// 设置代码页为 UTF-8
	cmd := exec.Command("chcp", "65001")
	err := cmd.Run()
	if err != nil {
		return false
	}

	cmd = exec.Command(ffmpegPath, "-i", rtspURL, "-frames:v", "1", "-f", "image2", "-y", "NUL")
	var out bytes.Buffer
	var errOut bytes.Buffer
	cmd.Stdout = &out
	cmd.Stderr = &errOut

	log.Printf("执行命令: %v", cmd.String())

	err = cmd.Run()
	if err != nil {
		log.Printf("通过FFMPEG检测摄像头是否在线失败: %v\n", err)
		log.Printf("标准输出: %s\n", out.String())
		log.Printf("标准错误: %s\n", errOut.String())
		return false
	}

	// 检查输出中是否包含成功获取帧的信息
	if strings.Contains(out.String(), "frame=    1") || strings.Contains(errOut.String(), "frame=    1") {
		log.Println("通过FFMPEG检测摄像头在线")
		return true
	}

	log.Println("通过FFMPEG检测摄像头不在线")
	return false
}

// 使用 ping 检测网络连通性
func checkOnlineByIP(ipAddress string) bool {
	// 设置代码页为 UTF-8
	cmd := exec.Command("chcp", "65001")
	err := cmd.Run()
	if err != nil {
		return false
	}

	// 根据操作系统选择 ping 命令参数
	var pingCmd *exec.Cmd
	if checkOsIsWindows() {
		pingCmd = exec.Command("ping", "-n", "1", ipAddress)
	} else {
		pingCmd = exec.Command("ping", "-c", "1", ipAddress)
	}

	var out bytes.Buffer
	var errOut bytes.Buffer
	pingCmd.Stdout = &out
	pingCmd.Stderr = &errOut

	log.Printf("执行命令: %v", pingCmd.String())

	err = pingCmd.Run()
	if err != nil {
		log.Printf("通过 ping IP 检测摄像头是否在线失败: %v\n", err)
		log.Printf("标准输出: %s\n", out.String())
		log.Printf("标准错误: %s\n", errOut.String())
		return false
	}

	// 检查输出中是否包含成功响应的信息
	if checkOsIsWindows() {
		if strings.Contains(out.String(), "Reply from "+ipAddress) { // 确保是目标 IP 的响应
			log.Println("通过 ping IP 检测摄像头在线")
			return true
		}
	} else {
		if strings.Contains(out.String(), "1 packets transmitted, 1 received") {
			log.Println("通过 ping IP 检测摄像头不在线")
			return true
		}
	}

	log.Println("网络不连通")
	return false
}

// 检测当前操作系统是否为 Windows
func checkOsIsWindows() bool {
	return strings.Contains(os.Getenv("OS"), "Windows_NT")
}

// 从 RTSP 地址中提取 IP 地址
func extractIPAddressFromRTSP(rtspURL string) (string, error) {
	// 使用正则表达式匹配 IP 地址，支持可能包含用户名和密码的情况
	re := regexp.MustCompile(`rtsp://[^@]*@?(\d+\.\d+\.\d+\.\d+):`)
	matches := re.FindStringSubmatch(rtspURL)
	if len(matches) < 2 {
		return "", fmt.Errorf("无法从 RTSP 地址中提取 IP 地址")
	}
	return matches[1], nil
}

// 初始化日志文件
func initLogFile(logDir string) (*os.File, string) {
	logFileName := time.Now().Format("2006-01-02") + ".log"
	logFilePath := filepath.Join(logDir, logFileName)
	logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		log.Fatalf("创建日志文件失败: %v\n", err)
	}
	return logFile, logFilePath
}

// 自定义日志输出
type multiWriter struct {
	file   *os.File
	stdout *os.File
}

func (mw *multiWriter) Write(p []byte) (n int, err error) {
	n, err = mw.file.Write(p)
	if err != nil {
		return
	}
	_, err = mw.stdout.Write(p)
	return
}

func main() {
	// 加载配置文件
	config, err := loadConfig("config.yml")
	if err != nil {
		fmt.Printf("加载配置文件失败: %v\n", err)
		return
	}
	log.Println("配置文件加载成功")

	// 初始化日志文件
	logDir := config.Logging.Dir
	if logDir == "" {
		logDir = "./log" // 默认日志目录
	}
	err = os.MkdirAll(logDir, 0755)
	if err != nil {
		fmt.Printf("创建日志目录失败: %v\n", err)
		return
	}
	logFile, logFilePath := initLogFile(logDir)

	// 设置日志格式
	mw := &multiWriter{file: logFile, stdout: os.Stdout}
	log.SetOutput(mw)
	log.SetFlags(log.LstdFlags | log.Lmicroseconds) // 添加微秒级时间戳

	// 摄像头 RTSP 地址
	rtspURL := config.Camera.RtspURL
	log.Printf("摄像头RTSP地址: %s", rtspURL)
	// 流媒体服务器 RTSP 地址
	rtspServerURL := config.Server.RtspURL
	log.Printf("流媒体服务器RTSP地址: %s", rtspServerURL)
	// FFmpeg 路径
	ffmpegPath := config.Ffmpeg.Path
	log.Printf("FFmpeg路径: %s", ffmpegPath)

	for {
		// 检查是否需要切换日志文件
		currentLogFilePath := filepath.Join(logDir, time.Now().Format("2006-01-02")+".log")
		if currentLogFilePath != logFilePath {
			logFile.Close()
			logFile, logFilePath = initLogFile(logDir)
			mw.file = logFile
		}

		log.Println("开始检测摄像头状态")
		// 根据配置文件中的检测方式选择检测方法
		isOnline := false
		switch config.Detection.Method {
		case "ffmpeg":
			isOnline = checkOnlineByFFMPEG(ffmpegPath, rtspURL)
		case "ping":
			ipAddress, err := extractIPAddressFromRTSP(rtspURL)
			if err != nil {
				log.Printf("从 RTSP 地址中提取 IP 地址失败: %v\n", err)
				continue
			}
			log.Printf("从 RTSP 地址中提取的 IP 地址: %s", ipAddress)
			isOnline = checkOnlineByIP(ipAddress)
		default:
			log.Println("未知的检测方式，跳过检测")
		}

		if isOnline {
			log.Println("摄像头在线，开始推流")
			// 设置代码页为 UTF-8
			cmd := exec.Command("chcp", "65001")
			cmd.Run()

			// 摄像头在线，开始推流
			cmd = exec.Command(ffmpegPath, "-rtsp_transport", "tcp", "-i", rtspURL, "-c", "copy", "-f", "rtsp", rtspServerURL)
			log.Printf("执行命令: %v", cmd.String())
			err := cmd.Start()
			if err != nil {
				log.Printf("推流失败: %v\n", err)
			} else {
				log.Println("正在推流...")
			}
			// 等待推流进程退出
			err = cmd.Wait()
			if err != nil {
				log.Printf("推流进程退出: %v\n", err)
			} else {
				log.Println("推流进程正常退出")
			}
		} else {
			log.Println("摄像头不在线，等待重连...")
		}

		// 每隔一段时间检测一次
		log.Println("等待5秒后再次检测")
		time.Sleep(5 * time.Second)
	}
}
